// Copyright (c) 2023 Presto Labs Pte. Ltd.
// Author: tianqiang

#include "coin2/exchange/bitmart/order_v3/parser.h"

namespace coin2::exchange::bitmart::order_v3 {

bool BitmartParser::IsBitmartSpotErrMessage(const JsonValue& doc) {
  auto error_code = GetErrorCode(doc);
  auto error_msg = GetErrorMsg(doc);

  return !error_code || !error_msg || error_code.value() != 1000 || error_msg.value() != "OK";
}

int64_t BitmartParser::GetProcOrderId(const JsonValue& data) {
  if (!data.HasMember("client_order_id") || !data["client_order_id"].IsString() ||
      data["client_order_id"].IsNull()) {
    return 0;
  }

  return stoll(data["client_order_id"].GetString());
}

coin::proto::OrderSide BitmartParser::GetOrderSide(const std::string& side) {
  if (side == "buy") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "sell") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

coin::proto::OrderType BitmartParser::GetOrderType(const std::string& type) {
  if (type == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "market") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

void BitmartParser::SetOrderStatus(ProductOrderElement* order, const std::string& status) {
  if ((status == "4") || (status == "5")) {
    order->set_is_live(true);
  } else if (status == "8" || status == "11") {
    order->set_is_live(false);
    order->set_cancel_confirmed(true);
  } else if (status == "6") {
    order->set_is_live(false);
    order->set_fully_filled(true);
  } else {
    LOG(ERROR) << "[SetOrderStatus] Unknown order status: " << status;
  }
}

void BitmartParser::SetWsOrderStatus(ProductOrderElement* order, const std::string& status) {
  if ((status == "4") || (status == "5")) {
    order->set_is_live(true);
  } else if (status == "8" || status == "12") {
    order->set_is_live(false);
    order->set_cancel_confirmed(true);
  } else if (status == "6") {
    order->set_is_live(false);
    order->set_fully_filled(true);
  } else {
    LOG(ERROR) << "[SetOrderStatus] Unknown order status: " << status;
  }
}

bool BitmartParser::IsOrderUpdate(const JsonValue& json) {
  if (!json.HasMember("table")) {
    return false;
  }

  return json["table"] == "spot/user/order";
}

void BitmartParser::ParseAccountBalance() {
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseAccountBalance] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(doc);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountBalance BitmartParser::ParseBalance(const JsonValue& doc) {
  RawBalanceMap raw_balance_map;
  auto success = ParseRawBalance(doc, &raw_balance_map);
  if (!success) {
    return {};
  }
  AccountBalance acc_balance;
  acc_balance.set_exchange("Bitmart");
  acc_balance.set_market_type("Spot");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
    DLOG(INFO) << "BALANCE: " << currency->native_currency() << " " << balance->total() << " "
               << balance->available();
  }
  is_account_balance_initialized_ = true;

  return acc_balance;
}

bool BitmartParser::ParseRawBalance(const JsonValue& doc, RawBalanceMap* raw_balance_map) {
  /*
  {
    "message":"OK",
    "code":1000,
    "trace":"ef834248-51d3-4223-9481-f862aa9dd39f",
    "data":{
      "wallet":[
        {
          "currency":"USDT",
          "name":"Tether USD",
          "available":"1000.00000000",
          "frozen":"0.00000000"
        },
        {
          "currency":"BTC",
          "name":"Bitcoin",
          "available":"10000.00000000",
          "frozen":"10.00000000"
        }
      ]
    }
  }
  */
  if (IsBitmartSpotErrMessage(doc)) {
    LOG(ERROR) << "[ParseBalance] Error message\n" << JsonToString(doc);
    return false;
  }

  if (!doc.HasMember("data") || !doc["data"].HasMember("wallet") ||
      !doc["data"]["wallet"].IsArray()) {
    LOG(ERROR) << "[ParseBalance] Invalid data format\n" << JsonToString(doc);
    return false;
  }

  for (const auto& data : doc["data"]["wallet"].GetArray()) {
    if (!data.HasMember("currency") || !data.HasMember("available") || !data.HasMember("frozen")) {
      continue;
    }
    CurrencyBalance currency_balance;
    currency_balance.set_currency_native(data["currency"].GetString());
    auto avail = stod(data["available"].GetString());
    auto hold = stod(data["frozen"].GetString());
    currency_balance.set_available(avail);
    currency_balance.set_total(avail + hold);
    currency_balance.set_hold(hold);
    raw_balance_map->emplace(currency_balance.currency_native(), currency_balance);
  }

  return true;
}

void BitmartParser::ParseOpenOrderInfo() { ParseOrderInfos(__func__, true); }

void BitmartParser::ParseHistoryOrderInfo() { ParseOrderInfos(__func__, false); }

void BitmartParser::ParseOrderInfos(std::string func_name, bool pagination) {
  /*
  {
    "message":"OK",
    "code":1000,
    "trace":"70e7d427-7436-4fb8-8cdd-97e1f5eadbe9",
    "data":{
      "orders":[
        {
          "order_id":"2147601241",
          "symbol":"BTC_USDT",
          "create_time":1591099963000,
          "side":"sell",
          "order_mode":"spot",
          "type":"limit",
          "price":"9000.00",
          "price_avg":"0.00",
          "size":"1.00000",
          "notional":"9000.00000000",
          "filled_notional":"0.00000000",
          "filled_size":"0.00000",
          "status":"4",
          "client_order_id":"bm4877624"
        }
      ]
    }
  }
  */
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[" + func_name + "] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsBitmartSpotErrMessage(doc)) {
    LOG(ERROR) << "[" + func_name + "] Error message\n" << JsonToString(doc);
    return;
  }

  if (!doc.HasMember("data") || !doc["data"].HasMember("orders") ||
      !doc["data"]["orders"].IsArray()) {
    LOG(ERROR) << "[" + func_name + "] Invalid data format\n" << JsonToString(doc);
    return;
  }

  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  bool product_order_bundle_found = false;
  const auto& orders = doc["data"]["orders"].GetArray();
  std::string symbol;
  for (const auto& data : orders) {
    if (!data.HasMember("symbol")) {
      continue;
    }
    std::string native_symbol = data["symbol"].GetString();
    if (!symcache_->has_product_native(native_symbol)) {
      continue;
    }
    auto order = product_order_bundle->add_each_order();
    ParseOrderInfo(order, data);
    symbol = native_symbol;
    product_order_bundle_found = true;
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }

  if (!pagination || orders.Size() < 200 || symbol.empty()) {
    return;
  }

  std::vector<int64_t> create_times;

  std::transform(orders.begin(), orders.end(), std::back_inserter(create_times), [](const auto& o) {
    return stoll(o["create_time"].GetString());
  });

  auto min_create_time = *std::min_element(create_times.begin(), create_times.end());

  std::pair<std::string, int64_t> symbol_and_end_time(symbol, min_create_time);

  LOG(INFO) << "open order return size:" << orders.Size() << " symbol:" << symbol
            << " min_create_time:" << min_create_time << " try to request next page!";
  mb_->PublishEvent("bitmart_spot/query_open_orders", &symbol_and_end_time);
}

void BitmartParser::ParseOrderInfo(ProductOrderElement* order, const JsonValue& data) {
  order->set_exchange_type(ExchangeType::Bitmart);
  order->set_market_type(MarketType::Spot);
  order->set_api_version("v3");
  order->set_exchange_order_id(data["order_id"].GetString());
  order->set_symbol(data["symbol"].GetString());
  order->set_price(stod(data["price"].GetString()));
  order->set_qty(std::fabs(stod(data["size"].GetString())));
  order->set_proc_order_id(GetProcOrderId(data));
  order->set_side(GetOrderSide(data["side"].GetString()));
  order->set_order_type(GetOrderType(data["type"].GetString()));
  order->set_timestamp(GetCurrentTimestamp());
  SetOrderStatus(order, data["status"].GetString());
}

void BitmartParser::ParseFillOrderInfo() {
  /*
  {
    "message":"OK",
    "code":1000,
    "trace":"a06a5c53-8e6f-42d6-8082-2ff4718d221c",
    "data":{
      "trades":[
        {
          "detail_id":"256348632",
          "order_id":"2147484350",
          "symbol":"BTC_USDT",
          "create_time":1590462303000,
          "side":"buy",
          "order_mode":"spot",
          "fees":"0.00001350",
          "fee_coin_name":"BTC",
          "notional":"88.00000000",
          "price_avg":"8800.00",
          "size":"0.01000",
          "exec_type":"M",
          "client_order_id":"bm476897"
        },
        ...
      ]
    }
  }
  */
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseFillOrderInfo] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsBitmartSpotErrMessage(doc)) {
    LOG(ERROR) << "[ParseFillOrderInfo] Error message\n" << JsonToString(doc);
    return;
  }

  if (!doc.HasMember("data") || !doc["data"].HasMember("trades") ||
      !doc["data"]["trades"].IsArray()) {
    LOG(ERROR) << "[ParseFillOrderInfo] Invalid data format\n" << JsonToString(doc);
    return;
  }

  auto product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();
  bool product_fill_bundle_found = false;
  for (const auto& data : doc["data"]["trades"].GetArray()) {
    auto fill = product_fill_bundle->add_each_fill();
    fill->Clear();
    ParseFillInfo(fill, data);
    product_fill_bundle_found = true;
  }

  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void BitmartParser::ParseFillInfo(ProductFillElement* fill, const JsonValue& data) {
  fill->set_symbol(data["symbol"].GetString());
  fill->set_exchange_order_id(data["order_id"].GetString());
  fill->set_proc_order_id(GetProcOrderId(data));
  fill->set_price(stod(data["price_avg"].GetString()));
  fill->set_qty(std::fabs(stod(data["size"].GetString())));
  fill->set_fill_id(data["detail_id"].GetString());
  fill->set_side(GetOrderSide(data["side"].GetString()));
  fill->set_fee(stod(data["fees"].GetString()));
  fill->set_fee_currency(data["fee_coin_name"].GetString());
  fill->set_fill_type(
      std::string(data["exec_type"].GetString()) == "M" ? coin::proto::FillType::MAKER_FILL_TYPE
                                                        : coin::proto::FillType::TAKER_FILL_TYPE);
  fill->set_fill_timestamp(GetCurrentTimestamp());
}

void BitmartParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(GetCurrentTimestamp());
  resp->set_success(false);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseSubmitResponse] AsioHttpContextStatus error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[ParseSubmitResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    return;
  }

  auto exchange_error_code = GetErrorCode(json);
  auto exchange_error_msg = GetErrorMsg(json);

  if (!exchange_error_code || !exchange_error_msg) {
    LOG(ERROR) << "[ParseSubmitResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    return;
  }

  // Error code: Internal Server Error
  // ignore response
  if (exchange_error_code.value() == 59002) {
    LOG(ERROR) << "[ParseSubmitResponse] Error code: Internal Server Error, ignore response\n"
               << http_context->res;
    result_.Clear();
    return;
  }

  auto error_code = GetOrderErrorCode(exchange_error_code.value());
  resp->set_error_code(error_code);
  resp->set_exchange_error_code(std::to_string(exchange_error_code.value()));
  resp->set_exchange_error_msg(exchange_error_msg.value());
  if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
    LOG(ERROR) << "[ParseSubmitResponse] Error response\n" << http_context->res;
    return;
  }

  if (!json.HasMember("data") || !json["data"].HasMember("order_id") ||
      !json["data"]["order_id"].IsString()) {
    LOG(ERROR) << "[ParseSubmitResponse] Invalid data format\n" << JsonToString(json);
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    return;
  }

  resp->set_exchange_order_id(json["data"]["order_id"].GetString());
  resp->set_success(true);
}

void BitmartParser::ParseCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(GetCurrentTimestamp());
  resp->set_success(false);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseCancelResponse] AsioHttpContextStatus error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[ParseCancelResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    return;
  }

  auto exchange_error_code = GetErrorCode(json);
  auto exchange_error_msg = GetErrorMsg(json);

  if (!exchange_error_code || !exchange_error_msg) {
    LOG(ERROR) << "[ParseCancelResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    return;
  }

  auto error_code = GetOrderErrorCode(exchange_error_code.value());
  resp->set_error_code(error_code);
  resp->set_exchange_error_code(std::to_string(exchange_error_code.value()));
  resp->set_exchange_error_msg(exchange_error_msg.value());
  if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
    LOG(ERROR) << "[ParseCancelResponse] Error response\n" << http_context->res;
    return;
  }

  if (!json.HasMember("data") || !json["data"].HasMember("result") ||
      !json["data"]["result"].IsBool() || !json["data"]["result"].GetBool()) {
    LOG(ERROR) << "[ParseCancelResponse] Invalid data format\n" << JsonToString(json);
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    return;
  }

  resp->set_success(true);
  resp->set_cancel_confirmed(true);
}

int32_t BitmartParser::ParseErrorMessage(const std::shared_ptr<AsioHttpContext>& http_context) {
  const auto http_code = http_context->res.result_int();
  if (http_code == 401) {
    LOG(ERROR) << "[ParseErrorMessage] Unauthorized-Authentication failed\n" << msg_->DebugString();
  } else if (http_code == 403) {
    LOG(ERROR) << "[ParseErrorMessage] Access denied\n" << msg_->DebugString();
  } else if (http_code == 404) {
    LOG(ERROR) << "[ParseErrorMessage] Request path not found\n" << msg_->DebugString();
  } else if (http_code == 429) {
    auto enable_time = 5 * 1'000'000'000LL + GetCurrentTimestamp();
    mb_->PublishEvent("bitmart/enable_time", reinterpret_cast<void*>(enable_time));
    LOG(ERROR) << "[ParseErrorMessage] Rate Limit Exceeded\n" << msg_->DebugString();
  } else if (http_code == 500) {
    LOG(WARNING) << "[ParseErrorMessage] BitMart service problem\n" << msg_->DebugString();
  }

  return http_code;
}

void BitmartParser::ParseWsOrderInfoList(const JsonDocument& document) {
  /*
  {
    "data":[
      {
        "symbol":"BTC_USDT",
        "side":"buy",
        "type":"market",
        "notional":"",
        "size":"1.0000000000",
        "ms_t":"1609926028000",
        "price":"46100.0000000000",
        "filled_notional":"46100.0000000000",
        "filled_size":"1.0000000000",
        "margin_trading":"0",
        "state":"4",
        "order_id":"2147857398",
        "order_type":"0",
        "last_fill_time":"1609926039226",
        "last_fill_price":"46100.00000",
        "last_fill_count":"1.00000",
        "exec_type":"M",
        "detail_id":"256348632",
        "client_order_id":"order4872191"
      }
    ],
    "table":"spot/user/order"
  }
  */
  if (!document.HasMember("data") || !document["data"].IsArray()) {
    return;
  }

  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();

  bool product_order_bundle_found = false;
  for (const auto& data : document["data"].GetArray()) {
    auto order = product_order_bundle->add_each_order();
    order->set_exchange_type(ExchangeType::Bitmart);
    order->set_market_type(MarketType::Spot);
    order->set_api_version("v3");
    order->set_exchange_order_id(data["order_id"].GetString());
    order->set_symbol(data["symbol"].GetString());
    order->set_price(stod(data["price"].GetString()));
    order->set_qty(std::fabs(stod(data["size"].GetString())));
    order->set_proc_order_id(GetProcOrderId(data));
    order->set_side(GetOrderSide(data["side"].GetString()));
    order->set_order_type(GetOrderType(data["type"].GetString()));
    order->set_timestamp(GetCurrentTimestamp());
    SetWsOrderStatus(order, data["state"].GetString());
    product_order_bundle_found = true;
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void BitmartParser::ParseWsTradeInfoList(const JsonDocument& document) {
  /*
  {
    "data":[
      {
        "symbol":"BTC_USDT",
        "side":"buy",
        "type":"market",
        "notional":"",
        "size":"1.0000000000",
        "ms_t":"1609926028000",
        "price":"46100.0000000000",
        "filled_notional":"46100.0000000000",
        "filled_size":"1.0000000000",
        "margin_trading":"0",
        "state":"4",
        "order_id":"2147857398",
        "order_type":"0",
        "last_fill_time":"1609926039226",
        "last_fill_price":"46100.00000",
        "last_fill_count":"1.00000",
        "exec_type":"M",
        "detail_id":"256348632",
        "client_order_id":"order4872191"
      }
    ],
    "table":"spot/user/order"
  }
  */
  if (!document.HasMember("data") || !document["data"].IsArray()) {
    return;
  }

  ProductFillBundle* product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();

  bool product_fill_bundle_found = false;
  for (const auto& data : document["data"].GetArray()) {
    if (!data.HasMember("detail_id")) {
      continue;
    }

    auto fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(data["symbol"].GetString());
    fill->set_price(stod(data["last_fill_price"].GetString()));
    fill->set_qty(std::fabs(stod(data["last_fill_count"].GetString())));
    fill->set_side(GetOrderSide(data["side"].GetString()));
    fill->set_fill_timestamp(GetCurrentTimestamp());
    fill->set_fill_id(data["detail_id"].GetString());
    fill->set_exchange_order_id(data["order_id"].GetString());
    fill->set_proc_order_id(GetProcOrderId(data));
    fill->set_fill_type(
        std::string(data["exec_type"].GetString()) == "M" ? coin::proto::FillType::MAKER_FILL_TYPE
                                                          : coin::proto::FillType::TAKER_FILL_TYPE);
    product_fill_bundle_found = true;
  }

  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void BitmartParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  if (IsOrderUpdate(document)) {
    ParseWsOrderInfoList(document);
    ParseWsTradeInfoList(document);
  }
}

auto BitmartParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg_->type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  CHECK(msg.type == OrderSubTopicId::REST_MESSAGE);

  ParseErrorMessage(msg.http_context);

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseAccountBalance();
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
      ParseOpenOrderInfo();
      break;
    case OrderSubTopicId::REST_DONE_ORDERS:
      ParseHistoryOrderInfo();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseFillOrderInfo();
      break;
    default: {
      LOG(ERROR) << "[ParseRawMessage] Unknown query type\n" << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::bitmart::order_v3
